package org.codefilarete.stalactite.sql.oracle;

import java.util.Collections;
import java.util.Set;
import java.util.function.LongSupplier;

import org.codefilarete.stalactite.engine.DatabaseVendorSettings;
import org.codefilarete.stalactite.engine.SQLOperationsFactories;
import org.codefilarete.stalactite.engine.SQLOperationsFactoriesBuilder;
import org.codefilarete.stalactite.mapping.id.sequence.DatabaseSequenceSelector;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.DMLNameProviderFactory;
import org.codefilarete.stalactite.sql.DatabaseSequenceSelectorFactory;
import org.codefilarete.stalactite.sql.GeneratedKeysReaderFactory;
import org.codefilarete.stalactite.sql.oracle.OracleDialectResolver.OracleDatabaseSignet;
import org.codefilarete.stalactite.sql.ddl.DDLSequenceGenerator;
import org.codefilarete.stalactite.sql.oracle.ddl.OracleDDLTableGenerator;
import org.codefilarete.stalactite.sql.ddl.SqlTypeRegistry;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.statement.ColumnParameterizedSQL;
import org.codefilarete.stalactite.sql.statement.DMLGenerator;
import org.codefilarete.stalactite.sql.statement.GeneratedKeysReader;
import org.codefilarete.stalactite.sql.statement.ReadOperationFactory;
import org.codefilarete.stalactite.sql.statement.WriteOperation;
import org.codefilarete.stalactite.sql.statement.WriteOperationFactory;
import org.codefilarete.stalactite.sql.oracle.statement.binder.OracleParameterBinderRegistry;
import org.codefilarete.stalactite.sql.oracle.statement.binder.OracleTypeMapping;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinderIndex;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.CaseInsensitiveSet;
import org.codefilarete.tool.collection.Iterables;

/**
 * 
 * @author Guillaume Mary
 */
public class OracleDatabaseSettings extends DatabaseVendorSettings {

	/**
	 * Oracle keywords, took from <a href="https://docs.oracle.com/cd/A97630_01/appdev.920/a42525/apb.htm">Oracle documentation</a> because those of
	 * it JDBC Drivers are not enough / accurate (see {@link oracle.jdbc.OracleDatabaseMetaData#getSQLKeywords()})
	 */
	@VisibleForTesting
	static final String[] KEYWORDS = new String[] {
			// Oracle Reserved Words
			"ACCESS", "ELSE", "MODIFY", "START",
			"ADD", "EXCLUSIVE", "NOAUDIT", "SELECT",
			"ALL", "EXISTS", "NOCOMPRESS", "SESSION",
			"ALTER", "FILE", "NOT", "SET",
			"AND", "FLOAT", "NOTFOUND", "SHARE",
			"ANY", "FOR", "NOWAIT", "SIZE",
			"ARRAYLEN", "FROM", "NULL", "SMALLINT",
			"AS", "GRANT", "NUMBER", "SQLBUF",
			"ASC", "GROUP", "OF", "SUCCESSFUL",
			"AUDIT", "HAVING", "OFFLINE", "SYNONYM",
			"BETWEEN", "IDENTIFIED", "ON", "SYSDATE",
			"BY", "IMMEDIATE", "ONLINE", "TABLE",
			"CHAR", "IN", "OPTION", "THEN",
			"CHECK", "INCREMENT", "OR", "TO",
			"CLUSTER", "INDEX", "ORDER", "TRIGGER",
			"COLUMN", "INITIAL", "PCTFREE", "UID",
			"COMMENT", "INSERT", "PRIOR", "UNION",
			"COMPRESS", "INTEGER", "PRIVILEGES", "UNIQUE",
			"CONNECT", "INTERSECT", "PUBLIC", "UPDATE",
			"CREATE", "INTO", "RAW", "USER",
			"CURRENT", "IS", "RENAME", "VALIDATE",
			"DATE", "LEVEL", "RESOURCE", "VALUES",
			"DECIMAL", "LIKE", "REVOKE", "VARCHAR",
			"DEFAULT", "LOCK", "ROW", "VARCHAR2",
			"DELETE", "LONG", "ROWID", "VIEW",
			"DESC", "MAXEXTENTS", "ROWLABEL", "WHENEVER",
			"DISTINCT", "MINUS", "ROWNUM", "WHERE",
			"DROP", "MODE", "ROWS", "WITH",
			
			// Oracle Keywords
			"ADMIN", "CURSOR", "FOUND", "MOUNT",
			"AFTER", "CYCLE", "FUNCTION", "NEXT",
			"ALLOCATE", "DATABASE", "GO", "NEW",
			"ANALYZE", "DATAFILE", "GOTO", "NOARCHIVELOG",
			"ARCHIVE", "DBA", "GROUPS", "NOCACHE",
			"ARCHIVELOG", "DEC", "INCLUDING", "NOCYCLE",
			"AUTHORIZATION", "DECLARE", "INDICATOR", "NOMAXVALUE",
			"AVG", "DISABLE", "INITRANS", "NOMINVALUE",
			"BACKUP", "DISMOUNT", "INSTANCE", "NONE",
			"BEGIN", "DOUBLE", "INT", "NOORDER",
			"BECOME", "DUMP", "KEY", "NORESETLOGS",
			"BEFORE", "EACH", "LANGUAGE", "NORMAL",
			"BLOCK", "ENABLE", "LAYER", "NOSORT",
			"BODY", "END", "LINK", "NUMERIC",
			"CACHE", "ESCAPE", "LISTS", "OFF",
			"CANCEL", "EVENTS", "LOGFILE", "OLD",
			"CASCADE", "EXCEPT", "MANAGE", "ONLY",
			"CHANGE", "EXCEPTIONS", "MANUAL", "OPEN",
			"CHARACTER", "EXEC", "MAX", "OPTIMAL",
			"CHECKPOINT", "EXPLAIN", "MAXDATAFILES", "OWN",
			"CLOSE", "EXECUTE", "MAXINSTANCES", "PACKAGE",
			"COBOL", "EXTENT", "MAXLOGFILES", "PARALLEL",
			"COMMIT", "EXTERNALLY", "MAXLOGHISTORY", "PCTINCREASE",
			"COMPILE", "FETCH", "MAXLOGMEMBERS", "PCTUSED",
			"CONSTRAINT", "FLUSH", "MAXTRANS", "PLAN",
			"CONSTRAINTS", "FREELIST", "MAXVALUE", "PLI",
			"CONTENTS", "FREELISTS", "MIN", "PRECISION",
			"CONTINUE", "FORCE", "MINEXTENTS", "PRIMARY",
			"CONTROLFILE", "FOREIGN", "MINVALUE", "PRIVATE",
			"COUNT", "FORTRAN", "MODULE", "PROCEDURE",
			
			// Oracle Keywords (continued):
			"PROFILE", "SAVEPOINT", "SQLSTATE", "TRACING",
			"QUOTA", "SCHEMA", "STATEMENT", "ID	TRANSACTION",
			"READ", "SCN", "STATISTICS", "TRIGGERS",
			"REAL", "SECTION", "STOP", "TRUNCATE",
			"RECOVER", "SEGMENT", "STORAGE", "UNDER",
			"REFERENCES", "SEQUENCE", "SUM", "UNLIMITED",
			"REFERENCING", "SHARED", "SWITCH", "UNTIL",
			"RESETLOGS", "SNAPSHOT", "SYSTEM", "USE",
			"RESTRICTED", "SOME", "TABLES", "USING",
			"REUSE", "SORT", "TABLESPACE", "WHEN",
			"ROLE", "SQL", "TEMPORARY", "WRITE",
			"ROLES", "SQLCODE", "THREAD", "WORK",
			"ROLLBACK", "SQLERROR", "TIME"
	};

	// Technical note: DO NOT declare settings BEFORE KEYWORDS field because it requires it and the JVM makes KEYWORDS null at this early stage (strange)
	public static final OracleDatabaseSettings ORACLE_23_0 = new OracleDatabaseSettings();

	private OracleDatabaseSettings() {
		this(new OracleSQLOperationsFactoriesBuilder(), new OracleParameterBinderRegistry());
	}
	
	private OracleDatabaseSettings(OracleSQLOperationsFactoriesBuilder sqlOperationsFactoriesBuilder, OracleParameterBinderRegistry parameterBinderRegistry) {
		super(new OracleDatabaseSignet(23, 0),
				Collections.unmodifiableSet(new CaseInsensitiveSet(KEYWORDS)),
				'"',
				new OracleTypeMapping(),
				parameterBinderRegistry,
				sqlOperationsFactoriesBuilder,
				new OracleGeneratedKeysReaderFactory(),
				1000,
				true);
	}

	private static class OracleSQLOperationsFactoriesBuilder implements SQLOperationsFactoriesBuilder {

		private final ReadOperationFactory readOperationFactory;
		private final OracleWriteOperationFactory writeOperationFactory;

		private OracleSQLOperationsFactoriesBuilder() {
			this.readOperationFactory = new ReadOperationFactory();
			this.writeOperationFactory = new OracleWriteOperationFactory();
		}
		
		private ReadOperationFactory getReadOperationFactory() {
			return readOperationFactory;
		}
		
		private OracleWriteOperationFactory getWriteOperationFactory() {
			return writeOperationFactory;
		}

		@Override
		public SQLOperationsFactories build(ParameterBinderIndex<Column, ParameterBinder> parameterBinders, DMLNameProviderFactory dmlNameProviderFactory, SqlTypeRegistry sqlTypeRegistry) {
			DMLGenerator dmlGenerator = new DMLGenerator(parameterBinders, DMLGenerator.NoopSorter.INSTANCE, dmlNameProviderFactory);
			OracleDDLTableGenerator ddlTableGenerator = new OracleDDLTableGenerator(sqlTypeRegistry, dmlNameProviderFactory);
			DDLSequenceGenerator ddlSequenceGenerator = new DDLSequenceGenerator(dmlNameProviderFactory);
			return new SQLOperationsFactories(writeOperationFactory, readOperationFactory, dmlGenerator, ddlTableGenerator, ddlSequenceGenerator, new OracleSequenceSelectorFactory(readOperationFactory));
		}
	}

	private static class OracleSequenceSelectorFactory implements DatabaseSequenceSelectorFactory {

		private final ReadOperationFactory readOperationFactory;

		private OracleSequenceSelectorFactory(ReadOperationFactory readOperationFactory) {
			this.readOperationFactory = readOperationFactory;
		}

		@Override
		public DatabaseSequenceSelector create(org.codefilarete.stalactite.sql.ddl.structure.Sequence databaseSequence, ConnectionProvider connectionProvider) {
			return new DatabaseSequenceSelector(databaseSequence, "select " + databaseSequence.getAbsoluteName() + ".nextval from dual", readOperationFactory, connectionProvider);
		}
	}
	
	/**
	 * {@link WriteOperationFactory} appropriate for Oracle : mainly indicates what columns must be retrieved while
	 * some generated key is expected.
	 *
	 * @author Guillaume Mary
	 * @see OracleGeneratedKeysReader
	 */
	@VisibleForTesting
	static class OracleWriteOperationFactory extends WriteOperationFactory {

		@Override
		public <T extends Table<T>> WriteOperation<Column<T, ?>> createInstanceForInsertion(ColumnParameterizedSQL<T> sqlGenerator,
																							ConnectionProvider connectionProvider,
																							LongSupplier expectedRowCount) {
			// Looking for autogenerated column (identifier policy is "after insertion") : it will be added to PreparedStatement descriptor
			Set<? extends Column<?, ?>> columns = ((ColumnParameterizedSQL<?>) sqlGenerator).getColumnIndexes().keySet();
			Column<?, ?> column = Iterables.find(columns, Column::isAutoGenerated);
			if (column != null) {
				return createInstance(sqlGenerator, connectionProvider,
						// Oracle requires passing the column name to be retrieved in the generated keys, else it gives back the RowId
						(connection, sql) -> connection.prepareStatement(sql, new String[] { column.getName() }), expectedRowCount);
			} else {
				// no autogenerated column => standard behavior
				return super.createInstanceForInsertion(sqlGenerator, connectionProvider, expectedRowCount);
			}
		}
	}
	
	/**
	 * Simple creator of {@link OracleGeneratedKeysReader}.
	 * 
	 * @author Guillaume Mary
	 */
	@VisibleForTesting
	static class OracleGeneratedKeysReaderFactory implements GeneratedKeysReaderFactory {
		
		@Override
		public <I> GeneratedKeysReader<I> build(String keyName, Class<I> columnType) {
			return (GeneratedKeysReader<I>) new OracleGeneratedKeysReader(keyName);
		}
	}
}
